from __future__ import annotations

import os
import time
from dataclasses import dataclass
from typing import Any

import requests

try:  # load environment variables automatically when python-dotenv is present
    from dotenv import load_dotenv  # type: ignore

    load_dotenv()
except Exception:
    pass

DEFAULT_TIMEOUT = 15


class ThingsBoardError(Exception):
    """Raised when ThingsBoard returns a non-success response."""


@dataclass
class DeviceInfo:
    id: str
    name: str
    type: str | None = None
    label: str | None = None


@dataclass
class AlarmInfo:
    id: str
    type: str
    severity: str
    status: str
    createdTime: int


@dataclass
class TelemetryValue:
    key: str
    ts: int
    value: Any


class ThingsBoardClient:
    """Thin ThingsBoard REST client consumed by the MCP/HTTP layers."""

    def __init__(
        self,
        host: str | None = None,
        port: int | None = None,
        jwt: str | None = None,
        username: str | None = None,
        password: str | None = None,
    ) -> None:
        self.host = host or os.getenv("TB_HOST", "services.skywant.cn")
        self.port = int(port or os.getenv("TB_PORT", "8080"))
        self._jwt = jwt or os.getenv("TB_JWT")
        self.username = username or os.getenv("TB_USERNAME")
        self.password = password or os.getenv("TB_PASSWORD")
        self._token_acquired_at: float | None = None
        self.session = requests.Session()

    @property
    def base_url(self) -> str:
        return f"http://{self.host}:{self.port}/api"

    def _auth_headers(self) -> dict[str, str]:
        token = self.get_jwt()
        return {"X-Authorization": f"Bearer {token}"}

    def get_jwt(self, force_refresh: bool = False) -> str:
        if self._jwt and not force_refresh:
            return self._strip_bearer(self._jwt)
        if not self.username or not self.password:
            raise ThingsBoardError("Username and password are required to refresh the JWT")
        url = f"{self.base_url}/auth/login"
        resp = self.session.post(
            url,
            json={"username": self.username, "password": self.password},
            timeout=DEFAULT_TIMEOUT,
        )
        if resp.status_code != 200:
            raise ThingsBoardError(f"Login failed: {resp.status_code} {resp.text}")
        data = resp.json()
        token = data.get("token")
        if not token:
            raise ThingsBoardError("Login response did not contain a token")
        self._jwt = token
        self._token_acquired_at = time.time()
        return self._strip_bearer(token)

    @staticmethod
    def _strip_bearer(token: str) -> str:
        if token.startswith("Bearer "):
            return token[len("Bearer ") :]
        return token

    def list_devices(self, page_size: int = 100, page: int = 0) -> list[DeviceInfo]:
        url = f"{self.base_url}/tenant/deviceInfos"
        params = {
            "pageSize": page_size,
            "page": page,
            "sortProperty": "createdTime",
            "sortOrder": "DESC",
        }
        resp = self.session.get(url, params=params, headers=self._auth_headers(), timeout=DEFAULT_TIMEOUT)
        if resp.status_code != 200:
            raise ThingsBoardError(f"Failed to fetch devices: {resp.status_code} {resp.text}")
        data = resp.json()
        items = data.get("data", [])
        result: list[DeviceInfo] = []
        for it in items:
            entity = it.get("deviceInfo") or it
            dev_id = entity.get("id", {}).get("id") or entity.get("id")
            result.append(
                DeviceInfo(
                    id=str(dev_id),
                    name=entity.get("name"),
                    type=entity.get("type"),
                    label=entity.get("label"),
                )
            )
        return result

    def get_device_alarms(self, device_id: str, page_size: int = 100, page: int = 0) -> list[AlarmInfo]:
        url = f"{self.base_url}/alarm/DEVICE/{device_id}"
        params = {
            "searchStatus": "ANY",
            "pageSize": page_size,
            "page": page,
            "sortProperty": "createdTime",
            "sortOrder": "DESC",
        }
        resp = self.session.get(url, params=params, headers=self._auth_headers(), timeout=DEFAULT_TIMEOUT)
        if resp.status_code != 200:
            raise ThingsBoardError(f"Failed to fetch alarms: {resp.status_code} {resp.text}")
        data = resp.json()
        items = data.get("data", [])
        result: list[AlarmInfo] = []
        for alarm in items:
            alarm_id = alarm.get("id", {}).get("id") or alarm.get("id")
            result.append(
                AlarmInfo(
                    id=str(alarm_id),
                    type=alarm.get("type"),
                    severity=alarm.get("severity"),
                    status=alarm.get("status"),
                    createdTime=alarm.get("createdTime", 0),
                )
            )
        return result

    def get_latest_telemetry(self, device_id: str) -> list[TelemetryValue]:
        keys_url = f"{self.base_url}/plugins/telemetry/DEVICE/{device_id}/keys/timeseries"
        keys_resp = self.session.get(keys_url, headers=self._auth_headers(), timeout=DEFAULT_TIMEOUT)
        if keys_resp.status_code != 200:
            raise ThingsBoardError(f"Failed to fetch telemetry keys: {keys_resp.status_code} {keys_resp.text}")
        keys = keys_resp.json()
        if not keys:
            return []
        latest_url = f"{self.base_url}/plugins/telemetry/DEVICE/{device_id}/values/timeseries"
        params = {"keys": ",".join(keys)}
        latest_resp = self.session.get(latest_url, params=params, headers=self._auth_headers(), timeout=DEFAULT_TIMEOUT)
        if latest_resp.status_code != 200:
            raise ThingsBoardError(f"Failed to fetch latest telemetry: {latest_resp.status_code} {latest_resp.text}")
        data = latest_resp.json()
        result: list[TelemetryValue] = []
        for key, arr in data.items():
            if not arr:
                continue
            latest_point = arr[-1]
            result.append(
                TelemetryValue(
                    key=key,
                    ts=latest_point.get("ts"),
                    value=latest_point.get("value"),
                )
            )
        return result

    def get_unconfirmed_alarms(self, page_size: int = 100, page: int = 0) -> list[AlarmInfo]:
        url = f"{self.base_url}/alarms"
        params = {
            "status": "ACTIVE_UNACK",
            "pageSize": page_size,
            "page": page,
            "sortProperty": "createdTime",
            "sortOrder": "DESC",
        }
        resp = self.session.get(url, params=params, headers=self._auth_headers(), timeout=DEFAULT_TIMEOUT)
        if resp.status_code != 200:
            raise ThingsBoardError(f"Failed to fetch unconfirmed alarms: {resp.status_code} {resp.text}")
        data = resp.json()
        items = data.get("data", [])
        result: list[AlarmInfo] = []
        for alarm in items:
            alarm_id = alarm.get("id", {}).get("id") or alarm.get("id")
            result.append(
                AlarmInfo(
                    id=str(alarm_id),
                    type=alarm.get("type"),
                    severity=alarm.get("severity"),
                    status=alarm.get("status"),
                    createdTime=alarm.get("createdTime", 0),
                )
            )
        return result

    def acknowledge_alarm(self, alarm_id: str) -> None:
        url = f"{self.base_url}/alarm/{alarm_id}/ack"
        resp = self.session.post(url, headers=self._auth_headers(), timeout=DEFAULT_TIMEOUT)
        if resp.status_code not in (200, 204):
            raise ThingsBoardError(f"Failed to acknowledge alarm {alarm_id}: {resp.status_code} {resp.text}")

    def clear_alarm(self, alarm_id: str) -> None:
        url = f"{self.base_url}/alarm/{alarm_id}/clear"
        resp = self.session.post(url, headers=self._auth_headers(), timeout=DEFAULT_TIMEOUT)
        if resp.status_code not in (200, 204):
            raise ThingsBoardError(f"Failed to clear alarm {alarm_id}: {resp.status_code} {resp.text}")


if __name__ == "__main__":
    client = ThingsBoardClient()
    try:
        devices = client.list_devices()
        print("device count:", len(devices))
        if devices:
            first = devices[0]
            print("first device:", first)
            alarms = client.get_device_alarms(first.id)
            print("alarm count:", len(alarms))
            telemetry = client.get_latest_telemetry(first.id)
            print("telemetry key count:", len(telemetry))
        unconfirmed = client.get_unconfirmed_alarms()
        print("unconfirmed alarm count:", len(unconfirmed))
    except Exception as exc:  # pragma: no cover - manual test helper
        print("integration test failed:", exc)
